package com.ctw.tinyservices.id.portal.log;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.ctw.tinyservices.id.portal.utils.ByteUtils;
import com.ctw.tinyservices.id.portal.utils.IpUtils;
import com.ctw.tinyservices.id.portal.utils.ThreadLocalUtils;
import com.ctw.tinyservices.id.portal.utils.TimeUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.*;

/**
 * @author TongWei.Chen 2022/04/21 20:05
 *
 * Request/Response Auto Print Log
 **/
@Component
@Aspect
@Order(-1)
public class ApiLogAspect {

    /**
     * 业务日志
     */
    private static final Logger LOG = LoggerFactory.getLogger(ApiLogAspect.class);
    private static final Logger REQ_LOG = LoggerFactory.getLogger("req-logger");
    private static final Logger SLOW_LOG = LoggerFactory.getLogger("slow-logger");

    @Value("${com.ctw.tinyservices.id.portal.log.slow_ms:1000}")
    private long logSlowMs;

    @Pointcut("@within(com.ctw.tinyservices.id.portal.log.ApiLog)")
    public void pointCut() {}

    @Before("pointCut()")
    public void before(JoinPoint joinPoint) {
        try {
            // 接收到请求，记录请求内容
            if(RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes){
                String reqId = UUID.randomUUID().toString();
                Object circleTimesStr = ThreadLocalUtils.get(ThreadLocalConstantEnum.CIRCLE_TIMES.getValue());
                if (circleTimesStr != null) {
                    Integer circleTms = Integer.valueOf(circleTimesStr.toString());
                    ThreadLocalUtils.set(ThreadLocalConstantEnum.CIRCLE_TIMES.getValue(), ++ circleTms);
                } else {
                    ThreadLocalUtils.set(ThreadLocalConstantEnum.CIRCLE_TIMES.getValue(), 0);
                    ThreadLocalUtils.set(ThreadLocalConstantEnum.REQUEST_ID.getValue(), reqId);
                    ThreadLocalUtils.set(ThreadLocalConstantEnum.START_TIME.getValue(), System.currentTimeMillis());
                }

                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                HttpServletRequest request = attributes.getRequest();
                // 记录请求内容
                JSONObject param = new JSONObject();
                param.put("requestId", reqId);
                param.put("requestURI",request.getRequestURI());
                param.put("method",request.getMethod());
                param.put("remoteAddr", IpUtils.getIpAddr(request));
                param.put("signature",joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
                param.put("contentType", request.getContentType());
                param.put("startTime", System.currentTimeMillis());
                // 获取所有请求参数
                Signature signature = joinPoint.getSignature();
                MethodSignature methodSignature = (MethodSignature) signature;
                String[] parameterNames = methodSignature.getParameterNames();
                Object[] args = joinPoint.getArgs();

                if (null != args && args.length > 0) {
                    HashMap<String, Object> paramMap = new HashMap<>();
                    for (int i = 0, length = parameterNames.length; i < length; i ++) {
                        if (args[i] instanceof MultipartFile) {
                            continue;
                        }
                        Annotation[] parameterAnnotations = methodSignature.getMethod().getParameterAnnotations()[i];
                        JSONObject paramJson;
                        if (checkType(args[i]) && checkSerialErrorType(args[i])) {
                            try {
                                paramJson = JSON.parseObject(JSON.toJSONString(args[i], SerializerFeature.WriteMapNullValue));
                                Field[] declaredFields = args[i].getClass().getDeclaredFields();
                                for (int j = 0, fieldsLength = declaredFields.length; j < fieldsLength; j ++) {
                                    IgnoreField annotation = declaredFields[j].getAnnotation(IgnoreField.class);
                                    if (null != annotation) {
                                        paramJson.remove(declaredFields[j].getName());
                                        continue;
                                    }
                                }

                                if (parameterAnnotations != null && parameterAnnotations.length > 0) {
                                    for (Annotation annotation : parameterAnnotations) {
                                        if (annotation instanceof IgnoreField) {
                                            IgnoreField ignoreField = (IgnoreField) annotation;
                                            if (null != ignoreField.value() && ignoreField.value().length > 0) {
                                                for (String ignoreValue : ignoreField.value()) {
                                                    paramJson.remove(ignoreValue);
                                                }
                                            }
                                        }
                                    }
                                }

                                paramMap.put(parameterNames[i], paramJson);
                            } catch (JSONException je) {
                                processCommonParam(parameterAnnotations, parameterNames[i], args[i], paramMap);
                            }

                        } else {
                            processCommonParam(parameterAnnotations, parameterNames[i], args[i], paramMap);
                        }
                    }
                    param.put("param", paramMap);
                }
                if (REQ_LOG.isInfoEnabled()) {
                    REQ_LOG.info("requestData：" + JSON.toJSONString(param, SerializerFeature.WriteMapNullValue));
                }
                if (LOG.isInfoEnabled()) {
                    LOG.info("requestData：" + JSON.toJSONString(param, SerializerFeature.WriteMapNullValue));
                }
            }
        } catch (Exception e) {
            LOG.warn("ApiLogAspect before occur error, reqId is [{}], error is ", ThreadLocalUtils.get(ThreadLocalConstantEnum.REQUEST_ID.getValue()), e);
        }
    }

    /**
     * 返回信息输出
     *
     * @param res 返回数据
     */
    @AfterReturning(returning = "res", pointcut = "pointCut()")
    public void doAfterReturning(Object res) {
        try {
            this.after(res, null);
        } finally {
            Object circleTimesStr = ThreadLocalUtils.get(ThreadLocalConstantEnum.CIRCLE_TIMES.getValue());
            if (circleTimesStr != null) {
                Integer circleTms = Integer.valueOf(circleTimesStr.toString());
                if (circleTms <= 0) {
                    ThreadLocalUtils.remove();
                } else {
                    ThreadLocalUtils.set(ThreadLocalConstantEnum.CIRCLE_TIMES.getValue(), --circleTms);
                }
            } else {
                ThreadLocalUtils.remove();
            }

        }
    }

    @AfterThrowing(pointcut = "pointCut()", throwing = "e")
    public void doAfterThrowing(Throwable e) {
        try {
            this.after(null, e);
        } finally {
            ThreadLocalUtils.remove();
        }
    }

    /**
     * 接口调用后处理
     * @param res 返回数据
     * @param e 异常
     */
    private void after(Object res, Throwable e){
        this.apiAfterLog(res, e);
    }

    /**
     * after日志输出
     * @param res
     */
    private void apiAfterLog(Object res, Throwable e){
        try {
            if(RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes){
                JSONObject response = new JSONObject();
                Runtime runtime = Runtime.getRuntime();
                long endTime = System.currentTimeMillis();
                long costTime = endTime - (long)ThreadLocalUtils.get(ThreadLocalConstantEnum.START_TIME.getValue());
                response.put("requestId", ThreadLocalUtils.get(ThreadLocalConstantEnum.REQUEST_ID.getValue()));
                response.put("response", res);
                response.put("endTime", endTime);
                response.put("spendTime", TimeUtils.formatDateAgo(costTime));
                response.put("totalMemory", ByteUtils.formatByteSize(runtime.totalMemory()));
                response.put("leftMemory", ByteUtils.formatByteSize(runtime.totalMemory() - runtime.freeMemory()));
                response.put("error", e);
                if (costTime >= logSlowMs) {
                    SLOW_LOG.warn("bizSlowLog：requestId: [{}] ，cost time is {}", ThreadLocalUtils.get(ThreadLocalConstantEnum.REQUEST_ID.getValue()), TimeUtils.formatDateAgo(costTime));
                }
                if (REQ_LOG.isInfoEnabled()) {
                    REQ_LOG.info("responseData: " + JSON.toJSONString(response, SerializerFeature.WriteMapNullValue));
                }
                if (LOG.isInfoEnabled()) {
                    LOG.info("responseData: " + JSON.toJSONString(response, SerializerFeature.WriteMapNullValue));
                }
            }

        } catch (Exception ex) {
            LOG.warn("ApiLogAspect apiAfterLog occur error, reqId is [{}], error is ", ThreadLocalUtils.get(ThreadLocalConstantEnum.REQUEST_ID.getValue()), ex);
        }
    }

    private void processCommonParam(Annotation[] parameterAnnotations, String paramName, Object arg, HashMap<String, Object> paramMap) {
        if (parameterAnnotations != null && parameterAnnotations.length > 0) {
            for (Annotation annotation : parameterAnnotations) {
                if (annotation instanceof IgnoreField) {
                    // 这就要求客户端需要将IgnoreFiled写到最前面
                    continue;
                } else {
                    if (! checkType(arg) && checkSerialErrorType(arg)) {
                        paramMap.put(paramName, arg);
                    }
                }
            }
        } else {
            if (! checkType(arg) && checkSerialErrorType(arg)) {
                paramMap.put(paramName, arg);
            }
        }
    }

    private boolean checkSerialErrorType(Object arg) {
        return ! (null == arg || arg instanceof ServletRequest || arg instanceof ServletResponse);
    }

    private boolean checkType(Object arg) {
        return ! (null == arg
                || arg instanceof Object[]
                || arg instanceof Collection
                || arg instanceof Map
                || arg instanceof Date
                || arg instanceof Enum
                || arg instanceof String
                || arg instanceof Byte
                || arg instanceof Character
                || arg instanceof Short
                || arg instanceof Integer
                || arg instanceof Long
                || arg instanceof Float
                || arg instanceof Double
                || arg instanceof Boolean);
    }

    private enum ThreadLocalConstantEnum {

        REQUEST_ID("REQUEST_ID"),
        START_TIME("START_TIME"),
        CIRCLE_TIMES("CIRCLE_TIMES")
        ;

        private String value;

        ThreadLocalConstantEnum(String value) {
            this.value = value;
        }

        public String getValue() {
            return value;
        }
    }
}
